package com.xy.webadmin.excel.controller;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.sax.handler.RowHandler;
import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson.JSONObject;
import com.xy.common.core.annotation.RelationConstDict;
import com.xy.common.core.annotation.RelationDict;
import com.xy.common.core.annotation.RelationGlobalDict;
import com.xy.common.core.annotation.UploadFlagColumn;
import com.xy.common.core.base.excel.BaseErrorExcelVo;
import com.xy.common.core.base.service.BaseService;
import com.xy.common.core.constant.ErrorCodeEnum;
import com.xy.common.core.object.CallResult;
import com.xy.common.core.object.ResponseResult;
import com.xy.common.core.upload.BaseUpDownloader;
import com.xy.common.core.upload.UpDownloaderFactory;
import com.xy.common.core.upload.UploadResponseInfo;
import com.xy.common.core.upload.UploadStoreInfo;
import com.xy.common.core.util.ApplicationContextHolder;
import com.xy.common.core.util.ImportUtil;
import com.xy.common.core.util.MyModelUtil;
import com.xy.common.log.annotation.OperationLog;
import com.xy.common.log.model.constant.SysOperationLogType;
import com.xy.webadmin.config.ApplicationConfig;
import com.xy.webadmin.excel.constant.ExcelConstant;
import com.xy.webadmin.excel.strategy.ExcelStrategy;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipException;

import static com.xy.common.core.util.ImportUtil.STRING_TYPE;
import static com.xy.common.core.util.ImportUtil.transDictData;

@Tag(name = "Excel通用接口")
@Slf4j
@RestController
@RequestMapping("/admin/app/excel")
public class ExcelController {
    @Resource
    private ApplicationConfig appConfig;
    @Resource
    private Map<String, ExcelStrategy<?>> excelStrategyMap;




    /**
     * 导入主表数据列表。
     *
     * @param importFile 上传的文件
     * @return 应答结果对象。
     */
    @OperationLog(type = SysOperationLogType.IMPORT, saveResponse = false)
    @PostMapping("/import")
    public ResponseResult<Object> importExcel(
            @RequestParam("className") String className,
            @RequestParam(value = "fieldName", required = false) String fieldName,
            @RequestParam("skipHeader") Boolean skipHeader,
            @RequestParam("importFile") MultipartFile importFile
    ) throws Exception {
        // 初始化错误excel日志文件名
        Date date = new Date(System.currentTimeMillis());

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(ExcelConstant.DATE_FORMAT_PATTERN);

        String errorExcelFile = simpleDateFormat.format(date) + ExcelConstant.EXCEL_EXTENSION_XLSX;

        List<BaseErrorExcelVo> errorExcelList = new ArrayList<>();

        //获取策略类
        ExcelStrategy<?> excelStrategy = excelStrategyMap.get(className);

        if (excelStrategy == null) {
            return ResponseResult.error(ErrorCodeEnum.EXCEL_STRATEGY_NOT_FOUND);
        }


        Class<?> clazz = excelStrategy.getStrategyClass();

        Class<?> errorExcelClass = excelStrategy.getErrorExcelClass();


        // 将excel压缩表解压到指定位置，并返回对应文件所处的路径，不包括文件名
        String filename = this.saveImportFile(appConfig.getUploadFileBaseDir(), importFile, true, className);

        // // 这里可以指定需要忽略导入的字段集合。如创建时间、创建人、更新时间、更新人、主键Id和逻辑删除，
        Set<String> ignoreFieldSet = this.getDefaultIgnoreFields();

        // 这里通过反射，根据实体对象，生成导入时需要的头信息数据,后面赋值要用,跟魔改后chineseName注解相关，没有chineseName注解的字段，不会被处理，就跟easyexcel里面的注解一样。
        List<ImportUtil.ImportHeaderInfo> headerInfoList = ImportUtil.makeHeaderInfoList(clazz, ignoreFieldSet);

        ImportUtil.ImportHeaderInfo[] headerInfos = headerInfoList.toArray(new ImportUtil.ImportHeaderInfo[]{});
        // // 2. 这里需要根据实际情况决定，导入文件中第一行是否为中文头信息，如果是可以跳过。这里我们默认为true。
        // // 这里根据自己的实际需求，为doImport的最后一个参数，传递需要进行字典转换的字段集合。
        // // 注意，集合中包含需要翻译的Java字段名，如: gradeId，具体可参考Employee策略类来实现其他类。
        Set<String> translatedDictFieldSet = new HashSet<>();

        excelStrategy.initTranslatedDict(translatedDictFieldSet);// 自定义那些字段要转化
        // 3.
        // 这里是导入的核心逻辑，根据传入的headerInfos、skipHeader、filename、clazz、translatedDictFieldSet、errorExcelList、strategy，进行导入操作，errorExcelList、strategy是为了汇总错误excel日志。

        AtomicInteger size = new AtomicInteger(0);


        List<?> dataList = this.doImport(headerInfos, skipHeader, filename, clazz, translatedDictFieldSet,
                errorExcelList, excelStrategy, size);

        // 4.
        // 这里是导入图片的逻辑，如果需要导入图片，请参考下面的代码，根据实际情况，修改导入图片的路径，目前逻辑图片不在excel文件里，而是软路径，用的方式，应该可以改为直接存在excel里。
        this.handleImage(dataList, clazz, appConfig.getUploadFileBaseDir(), errorExcelList, excelStrategy, fieldName,
                filename); // 新增图片赋值

        // 5. 这里是导入时，对数据进行验证的逻辑，如果需要验证：比如唯一啊，等等...，请参考下面的代码，根据实际情况，修改验证逻辑。
        CallResult result = excelStrategy.verifyImportList(dataList, translatedDictFieldSet, errorExcelList);

        // 最终留下来的都是合法数据，自行操作，一般在saveNewBatch操作就好了
        if (!result.isSuccess()) {
            // result中返回了具体的验证失败对象，如果需要返回更加详细的错误，可根据实际情况手动修改。
            return ResponseResult.errorFrom(result);
        }
        excelStrategy.saveNewBatch(dataList, -1);

        JSONObject resultJson = new JSONObject();

        resultJson.put("failNumber", errorExcelList.size());

        resultJson.put("successNumber", size.get() - errorExcelList.size());
        // 这里的返回的excel的类自己定义，

        if (!errorExcelList.isEmpty()) {
            String prePath = appConfig.getUploadFileBaseDir() + File.separator + ExcelConstant.ERROR_EXCEL_DIR + File.separator
                    + className;
            File file = new File(prePath);
            if (!file.exists()) {
                file.mkdirs();
            }
            EasyExcel.write(prePath + File.separator + errorExcelFile, errorExcelClass).sheet(ExcelConstant.EXCEL_SHEET_NAME)
                    .doWrite(errorExcelList);
            resultJson.put("url", prePath + File.separator + errorExcelFile);
            return ResponseResult.success(resultJson);
        } else {
            return ResponseResult.success(resultJson);
        }

    }

    private void handleImage(List<?> dataList, Class<?> clazz, String rootPath, List<BaseErrorExcelVo> errorExcelList, ExcelStrategy<?> strategy, String fieldName, String filename) throws IOException {
        if (dataList == null || dataList.isEmpty()) {
//            strategy.handleErrorExcel(errorExcelList, "没有数据", null);
            return;
        }
        //木有图片 就不做处理了
        if (StrUtil.isBlank(fieldName)) {
            return;
        }
        //获取这个类的UploadFlagColumn注解对应的字段
        //获取fieldName在这个类上是否有UploadFlagColumn注解
        Field field = ReflectUtil.getField(clazz, fieldName);

        if (field == null) {
            strategy.handleErrorExcel(errorExcelList, "没有" + fieldName + "字段", null);
            UploadFlagColumn annotation = field.getAnnotation(UploadFlagColumn.class);
            if (annotation == null) {
                strategy.handleErrorExcel(errorExcelList, "字段" + fieldName + "没有UploadFlagColumn注解", null);
            }
            return;
        }


        UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(clazz, fieldName);
        // 这里就会判断参数中指定的字段，是否支持上传操作。
        if (!storeInfo.isSupportUpload()) {
            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
                    ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD));
            strategy.handleErrorExcel(errorExcelList, "字段不支持上传", null);

            return;
        }
        // 根据字段注解中的存储类型，通过工厂方法获取匹配的上传下载实现类，从而解耦。
        UpDownloaderFactory upDownloaderFactory = SpringUtil.getBean(UpDownloaderFactory.class);
        BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
        for (Object data : dataList) {
            //获取照片字段的值
            String fieldValue = (String) ReflectUtil.getFieldValue(data, fieldName);


            //值为空，没有照片，就跳过了，后面还有验证阶段统一验证值是否必须，不在这里搞
            if (StrUtil.isEmpty(fieldValue)) {
                continue;
            }

            //拼接filename
            fieldValue = filename + File.separator + fieldValue;


            UploadResponseInfo responseInfo;
            responseInfo = upDownloader.doUpload(null,
                    rootPath, clazz.getSimpleName(), fieldName, true, fieldValue);
            String uploadPath = responseInfo.getSavePath();//这里不一样 橙单格式是[{"name":"b.jpg","downloadUri":"/admin/app/download","filename":"b9a31b08c8a843af9aabdda489c7fdf4.jpg","uploadPath":"./zz-resource/upload-files/app/image/Employee/photoUrl"}]
            ReflectUtil.setFieldValue(data, fieldName, uploadPath);
        }
    }


    private static String saveImportFile(String uploadFileBaseDir, MultipartFile importFile, boolean isZip, String className) throws Exception {
        if (isZip) {
//            String uploadBaseDir = uploadFileBaseDir.substring(2);
            String uploadBaseDir = uploadFileBaseDir.substring(1);
            uploadBaseDir = System.getProperty("user.dir") + uploadBaseDir;//这里要用绝对路径，=如果用相对路径，写入的地方是在编译文件里面（也就是项目名/target/class）了，不是在项目文件里
            // 目标目录路径：uploadFileBaseDir/className/UUID
            String targetDirPath = uploadBaseDir + File.separator + className + File.separator + UUID.fastUUID().toString();

            // 创建目标目录
            File targetDir = new File(targetDirPath);
            if (!targetDir.exists()) {
                boolean dirCreated = targetDir.mkdirs();
                if (!dirCreated) {
                    log.error("Failed to create target directory: " + targetDirPath);
                    return null;
                }
            }

            // 检查上传文件是否为空
            if (importFile.isEmpty()) {
                log.error("Uploaded file is empty.");
                return null;
            }

            // 将上传的 ZIP 文件写入目标目录
            String zipFilePath = targetDirPath + File.separator + importFile.getOriginalFilename();
            log.info("Writing uploaded file to: " + zipFilePath);

            // 确保目标文件路径是唯一的
            File zipFile = new File(zipFilePath);
            if (zipFile.exists()) {
                log.error("File already exists: " + zipFilePath);
                return null;
            }

            // 写入文件
            System.out.println("文件大小" + importFile.getSize());
            byte[] fileBytes = importFile.getBytes();
            if (fileBytes == null || fileBytes.length == 0) {
                log.error("File bytes are empty.");
                return null;
            }

            File file = FileUtil.writeBytes(fileBytes, zipFilePath);
            System.out.println(file.getAbsoluteFile());
            log.info("File successfully written to: " + zipFilePath);
            // 解压 ZIP 文件到目标目录
            log.info("Unzipping file to: " + targetDirPath);
            Process unzipProcess = Runtime.getRuntime().exec("unzip " + zipFilePath + " -d " + targetDirPath);
            boolean unzipFinished = unzipProcess.waitFor(10, TimeUnit.SECONDS);
            if (!unzipFinished) {
                log.error("Unzip process timed out.");
                return null;
            }

            // 检查解压是否成功
            if (unzipProcess.exitValue() != 0) {
                log.error("Unzip process failed with exit code: " + unzipProcess.exitValue());
                throw new ZipException("Unzip process failed with exit code: " + unzipProcess.exitValue() + "如果有中文，请改成英文");
            }

            // 删除压缩包
            log.info("Deleting zip file: " + zipFilePath);
            FileUtil.del(zipFilePath);

            // 查找解压后的子文件夹
            File[] subDirs = targetDir.listFiles(File::isDirectory);
            if (subDirs == null || subDirs.length == 0) {
                log.error("No subdirectory found in the extracted files.");
                return null;
            }

            // 进入第一个子文件夹（假设只有一个子文件夹）
            File subDir = subDirs[0];
            log.info("Entering subdirectory: " + subDir.getAbsolutePath());

            // 查找子文件夹中的 Excel 文件（只有一个 Excel 文件）
            File[] files = subDir.listFiles((dir, name) -> name.endsWith(ExcelConstant.EXCEL_EXTENSION_XLSX));
            if (files != null && files.length == 1) {
                // 返回 Excel 文件所在的路径

                return files[0].getParent();
            } else {
                log.error("Expected exactly one Excel file, but found " + (files == null ? 0 : files.length) + " files.");
            }

        }
        return null;
    }


    private  List<?> doImport(ImportUtil.ImportHeaderInfo[] headers, Boolean skipHeader, String filename, Class<?> clazz, Set<String> translateDictFieldSet, List<?> errorExcelList, ExcelStrategy strategy, AtomicInteger size) {
        // 这里将需要进行字典反向翻译的字段类型改为String，否则使用原有的字典Id类型时，无法正确执行下面的doImport方法。
        if (CollUtil.isNotEmpty(translateDictFieldSet)) {
            for (ImportUtil.ImportHeaderInfo header : headers) {
                if (translateDictFieldSet.contains(header.getFieldName())) {
                    header.setFieldType(STRING_TYPE);
                }
            }
        }
        List<Map<String, Object>> resultList = this.doImport(headers, skipHeader, filename);
        size.set(resultList.size());
        if (CollUtil.isNotEmpty(translateDictFieldSet)) {
            translateDictFieldSet.forEach(c -> doTranslateDict(resultList, clazz, c, errorExcelList, strategy));
        }
        return MyModelUtil.mapToBeanList(resultList, clazz);
    }

    private static void doTranslateDict(List<Map<String, Object>> dataList, Class<?> modelClass, String fieldName, List<?> errorExcelList, ExcelStrategy strategy) {

        if (CollUtil.isEmpty(dataList)) {
            return;
        }
        Field field = ReflectUtil.getField(modelClass, fieldName + "DictMap");
        Assert.notNull(field);
        if(ExcelConstant.FIELD_DEPT_ID.equals(fieldName) || ExcelConstant.FIELD_COMPANY_ID.equals(fieldName)){

            strategy.handleDeptIdSearch(dataList,fieldName,errorExcelList);
            return;
        }

        Map<String, Object> inversedDictMap;
        boolean multiSelect;
        if (field.isAnnotationPresent(RelationConstDict.class)) {
            RelationConstDict r = field.getAnnotation(RelationConstDict.class);
            multiSelect = r.multiSelect();
            Field f = ReflectUtil.getField(r.constantDictClass(), "DICT_MAP");
            Map<Object, String> dictMap = (Map<Object, String>) ReflectUtil.getStaticFieldValue(f);
            inversedDictMap = MapUtil.inverse(dictMap);
        } else if (field.isAnnotationPresent(RelationDict.class)) {
            RelationDict r = field.getAnnotation(RelationDict.class);
            multiSelect = r.multiSelect();
            String slaveServiceName = r.slaveServiceName();
            if (StrUtil.isBlank(slaveServiceName)) {
                slaveServiceName = r.slaveModelClass().getSimpleName() + "Service";
            }
            BaseService<Object, Serializable> service =
                    ApplicationContextHolder.getBean(StrUtil.lowerFirst(slaveServiceName));
            List<Object> dictDataList = service.getAllList();
            List<Map<String, Object>> dataMapList = MyModelUtil.beanToMapList(dictDataList);
            inversedDictMap = new HashMap<>(dataMapList.size());
            dataMapList.forEach(d ->
                    inversedDictMap.put(d.get(r.slaveNameField()).toString(), d.get(r.slaveIdField())));
        } else if (field.isAnnotationPresent(RelationGlobalDict.class)) {
            RelationGlobalDict r = field.getAnnotation(RelationGlobalDict.class);
            multiSelect = r.multiSelect();
            BaseService<Object, Serializable> s = ApplicationContextHolder.getBean("globalDictService");
            Method m = ReflectUtil.getMethodByName(s.getClass(), "getGlobalDictItemDictMapFromCache");
            Map<Object, String> dictMap = ReflectUtil.invoke(s, m, r.dictCode(), null);
            inversedDictMap = MapUtil.inverse(dictMap);
        } else {
            strategy.handleErrorExcel(errorExcelList, ExcelConstant.ERROR_PREFIX_PROGRAM + "字段名有误:" + fieldName + " 错误原因：Only Support RelationConstDict and RelationDict Field", null);
            dataList.clear();//清空列表，字段名错了，就别新增了
            return;
//            throw new UnsupportedOperationException("Only Support RelationConstDict and RelationDict Field");
        }
        if (MapUtil.isEmpty(inversedDictMap)) {
            strategy.handleErrorExcel(errorExcelList, ExcelConstant.ERROR_PREFIX_PROGRAM + "字典" + inversedDictMap + "数据为空", null);
//            log.warn("Dict Data List is EMPTY.");
            dataList.clear();
            return;
        }
        Iterator<Map<String, Object>> iterator = dataList.iterator();
        while (iterator.hasNext()) {
            Map<String, Object> data = iterator.next();
            Object value = data.get(fieldName);
            if (value != null) {
                Object newValue = transDictData(multiSelect, value, inversedDictMap);
                if (newValue == null) {
                    strategy.handleErrorExcel(errorExcelList, ExcelConstant.ERROR_PREFIX_DATA_MISSING + "字典" + value + "在数据库没有匹配值", JSONObject.toJSONString(data));
                    iterator.remove(); // 使用迭代器的 remove 方法安全地移除元素
                } else {
                    data.put(fieldName, newValue); // 更新字段值
                }
            }
        }

    }


    private  List<Map<String, Object>> doImport(ImportUtil.ImportHeaderInfo[] headers, Boolean skipHeader, String filename) {
        //找一下当前文件夹下的xlsx或者xlsx文件
        File folder = new File(filename);
        File[] files = folder.listFiles((dir, name) -> name.endsWith(ExcelConstant.EXCEL_EXTENSION_XLSX) || name.endsWith(ExcelConstant.EXCEL_EXTENSION_XLS));

        // 如果找到文件，返回第一个文件的路径
        if (files != null && files.length > 0) {
            filename = files[0].getAbsolutePath();
        }

        Assert.notNull(headers);
        Assert.isTrue(StrUtil.isNotBlank(filename));


        List<Map<String, Object>> resultList = new LinkedList<>();

        RowHandler rowHandler = ImportUtil.createRowHandler(headers, skipHeader, resultList);

        ExcelUtil.readBySax(new File(filename), 0, rowHandler);

        return resultList;
    }


    /**
     * 获取默认需要忽略的字段集合。
     */
    private Set<String> getDefaultIgnoreFields() {
        Set<String> ignoreFieldSet = new HashSet<>();
        ignoreFieldSet.add(ExcelConstant.FIELD_CREATE_TIME);
        ignoreFieldSet.add(ExcelConstant.FIELD_UPDATE_TIME);
        ignoreFieldSet.add(ExcelConstant.FIELD_CREATE_USER_ID);
        ignoreFieldSet.add(ExcelConstant.FIELD_UPDATE_USER_ID);
        return ignoreFieldSet;
    }

}
